Sound Manager 3.3 Release Notes
Here is the release notes for the current Sound Manager 3.3. In addition to bug fixes, the most notable new feature for this release is the ability to schedule sounds to be played at a specific time. To further aid in development efforts by our developers, we will be providing a debugging version of the Sound Manager. This debugging version will have asserts in the code which will report any errors during execution. The idea is that this will help developers locate bugs in their code quickly and easily.
A major new feature of Sound Manager 3.3 is the ability to schedule a sound. The mixer supports scheduling sounds in the past and future. If the start time is in the past, then the mixer will fast forward through the source at non-interrupt time attempting to catch up with the current time. Once there, the sounds included into the mix. If the sound is in the future or the past, the mixer will begin mixing the source into the output stream at the specified time within a sample of accuracy.
There are two new sound commands for supporing the sound clock; clockComponentCmd and getClockComponentCmd. These commands are used by QuickTime to obtain the sound clock which is used as the main time base. The Sound Manager sound clock is only available when QuickTime is installed. This allows QuickTime's video to be synchronized with the audio stream, and can be used to synchronize multiple audio sources as well. There is one sound clock per source (e.g. sound channel) which is updated with each hardware buffer. The sound clock counts samples being consumed by the hardware deivce as it is pass from the Mixer.
Once the clock ComponentInstance has been obtained, any and all of the QuickTime clock component calls (e.g. ClockGetTime) can be used. Below is an example of scheduling two sounds with the sound clock. It assumes that the SoundHeader of the ScheduledSoundHeader struct has already been established. Note that although each channel has it's own clock, both are synchronized with the mixer. Therefore obtaining the current time from one channel will result in the same time for the second. This allows both channels to use the same start time.
Note that a sound channel can only have one scheduled sound at any given time. The Mixer does not queue up additional scheduled sounds. You can stream a sound continuously by starting the first buffer with the scheduledSoundCmd, then use the bufferCmd and callBackCmd sequence with SndDoCommand() to stream additional samples. This has been the techniqued used in the past. By replacing the first bufferCmd with the new scheduledSoundCmd you can cause the samples to start playing at a specific point in time.
The Sound Manager sources are now multiplatform. They are built for the Mac OS, Windows 95, Windows NT, and soon could be for Unix platforms. This means the same Sound Manager is available for each of these platforms, and contains the same API and features. This brings into being the issue of "endianness". Basically, endian conversion is treated exactly as a compression conversion. Any non-native endian format will be required to be "decompressed" into the native format. All audio decompressors (e.g. MACE, IMA, uLaw, aLaw) must decompress into the native format of the platform. There are three distinct groups to be considered here; 1. a program playing audio, 2. a sound component manipulating the audio, and 3. the output device sending the audio to a hardware device.
For applications playing audio for any non-native endian formats (e.g. 16 bit Little Endian on the Macintosh) use the CmpSoundHeader, SndDoubleBufferHeader2, or SoundComponentData with the format set to k16BitLittleEndianFormat. This causes the Sound Manager to install the 16 Bit Little Endian codec into the sound channel. (Note that the SoundHeader, ExtSoundHeader, and SndDoubleBufferHeader struct do not have a format field.)
For sound component developers, there are two cases to consider; the compressor and decompressor components. The uncompressed formats are assumed to be in native format of the platform. In other words, the input to a compressor is in native format and all output from a decompressor is in native format.
For sound output devices, the mixer is generating audio in the platform's native format. If the hardware requires non-native format, it's the responsibility of the output device to convert to the native format.
All of the non-compressed formats supported by Apple will also support both big and little endian format. The Apple sound components will assume big endian format, unless otherwise configured using the siDecompressionParams or siCompressionParams selector. Decompression components should always output native format, and default to big endian for their input. Compression components should default to big endian format for output, and consume native endian for input.
- struct SoundSlopeAndInterceptRecord {
- Float64 slope;
- Float64 intercept;
- Float64 minClip;
- Float64 maxClip;
- };
void *atoms;
- err = CreateAudioAtomsList(&atoms, k32BitFormat, false);
- if (err != noErr) return (err);
err = SndSetInfo(chan, siDecompressionParams, atoms);
short hasDialog;
- err = SoundComponentGetInfo(compressor, nil, siOptionsDialog, &hasDialog);
- if (err != noErr) return (err);
- if (hasDialog)
- {
- err = SoundComponentSetInfo(compressor, nil, siOptionsDialog, nil);
- if (err != noErr) return (err);
- }
The new sound commands that have been added include: clockComponentCmd, getClockComponentCmd, scheduledSoundCmd, and linkSoundComponentsCmd. These sound commands are used with the SndDoImmediate() and SndDoCommand() functions.
- /* ScheduledSoundHeader flags*/
- enum {
- kScheduledSoundDoScheduled = 1 << 0,
- kScheduledSoundDoCallBack = 1 << 1
- };
- struct ScheduledSoundHeader {
- SoundHeaderUnion u;
- long flags;
- short reserved;
- short callBackParam1;
- long callBackParam2;
- TimeRecord startTime;
- };
SoundInput.h and SoundComponents.h have been merged into Sound.h. The former two interface files are now empty, but simply include the later.
Previously defined in SoundComponents.h were a set of component sub-types which were used in the 'thng' resource of an audio codec. This OSType was matched with a given sound's format, which determined the codec which was necessary to support the given sound. Unfortunately, the usage of these constants were not obvious to many and are required as the value set in the "format" parameter of the functions GetCompressionInfo(), GetCompressionName (), SetupSndHeader (), and SetupAIFFHeader (), and in the CmpSoundHeader, SndDoubleBufferHeader2, CompressionInfo and SoundComponentData stucts. A new set of constants have been created to replace these with the addition of the recently supported formats.
The Sound Manager provides a routine, SndSoundManagerVersion(), which returns the currently installed version. Many developers have been confused as to how to determine which Sound Manager is installed. Below is the simpliest method to check for Sound Manager 3.1 or later, use the following code. Note that the NumVersionVariant struct is being used from the lastest version of the MacTypes interface file. This is a union of the old NumVersion struct with its various parts, and a 32 bit long value which allows for easy comparisons.
- Boolean HasSoundManager3_1(void)
- {
- NumVersionVariant version;
- version.parts = SndSoundManagerVersion();
- return (version.whole >= 0x03100000) // version 3.1
- }
- OSErr ConvertToLittleEndian(Ptr inputPtr, Ptr outputPtr)
- {
- SoundConverter sc;
- SoundComponentData inputFormat, outputFormat;
- unsigned long inputFrames, inputBytes;
- unsigned long outputFrames, outputBytes;
- void *littleEndianAtomsList;
- OSErr err;
- inputFormat.flags = 0;
- inputFormat.format = k16BitBigEndianFormat;
- inputFormat.numChannels = 1;
- inputFormat.sampleSize = 16;
- inputFormat.sampleRate = rate22050hz;
- inputFormat.sampleCount = 0;
- inputFormat.buffer = nil;
- inputFormat.reserved = 0;
- outputFormat.flags = 0;
- outputFormat.format = k32BitFormat;
- outputFormat.numChannels = 1;
- outputFormat.sampleSize = 16;
- outputFormat.sampleRate = rate22050hz;
- outputFormat.sampleCount = 0;
- outputFormat.buffer = nil;
- outputFormat.reserved = 0;
- err = SoundConverterOpen(&inputFormat, &outputFormat, &sc);
- if (err != noErr) return (err);
- err = SoundConverterGetBufferSizes(sc, kOurInputBytesTarget, &inputFrames,
- &inputBytes, &outputBytes);
- if (err != noErr) return (err);
- err = CreateAudioAtomsList(k32BitFormat, true, &littleEndianAtomsList);
- if (err != noErr) return (err);
- err = SoundConverterSetInfo(sc, siCompressionParams, littleEndianAtomsList);
- if (err != noErr) return (err);
- err = SoundConverterBeginConversion(sc);
- if (err != noErr) return (err);
- err = SoundConverterConvertBuffer(sc, inputPtr, inputFrames, outputPtr,
- &outputFrames, &outputBytes);
- if (err != noErr) return (err);
- err = SoundConverterEndConversion(sc, outputPtr, &outputFrames, &outputBytes);
- if (err != noErr) return (err);
- err = SoundConverterClose(sc);
- if (err != noErr) return (err);
- }
Atoms are aways in big endian format. The atoms contained in this list can be in any order ending with the AudioTerminatorAtom. No assumptions should be made about reading an audio atom list, other than the last atom is the AudioTerminatorAtom. Using SndGetInfo() and the siDecompressionParams selector will return a list of atoms of any possible ordering.
- OSErr CreateAudioAtomsList(OSType format, Boolean littleEndian,
- void **littleEndianAtomsList)
- {
- typedef struct {
- AudioFormatAtom formatData;
- AudioEndianAtom endianData;
- AudioTerminatorAtom terminatorData;
- } AudioDecompressionAtoms, *AudioDecompressionAtomsPtr;
- atoms = (AudioDecompressionAtomsPtr)NewPtr(sizeof(AudioDecompressionAtoms));
- if (atoms == nil)
- return (MemError());
- atoms->formatData.size = EndianU32_NtoB(sizeof(AudioFormatAtom));
- atoms->formatData.atomType = EndianU32_NtoB(kAudioFormatAtomType);
- atoms->formatData.format = EndianU32_NtoB(format);
- atoms->endianData.size = EndianU32_NtoB(sizeof(AudioEndianAtom));
- atoms->endianData.atomType = EndianU32_NtoB(kAudioEndianAtomType);
- atoms->endianData.littleEndian = EndianU16_NtoB(littleEndian);
- atoms->terminatorData.size = EndianU32_NtoB(sizeof(AudioTerminatorAtom));
- atoms->terminatorData.atomType = EndianU32_NtoB(kAudioTerminatorAtomType);
- *littleEndianAtomsList = atoms;
- return (noErr);
- }
- Boolean GetFormatAndEndianFromAtomsList(UserDataAtom *atom, OSType *format,
- Boolean *littleEndian)
- {
- Boolean moreAtoms;
- moreAtoms = true;
- do
- {
- if (EndianS32_BtoN(atom->size) < 8)
- return (false); // bad atom size
- switch (EndianU32_BtoN(atom->atomType))
- {
- case kAudioFormatAtomType:
- *format = EndianU32_BtoN(((AudioFormatAtom *)atom)->format);
- break;
- case kAudioEndianAtomType:
- *littleEndian = EndianU16_BtoN(((AudioEndianAtom *)atom)->littleEndian);
- break;
- case kAudioTerminatorAtomType:
- moreAtoms = false;
- break;
- default: // unknown atom type
- break;
- }
- atom = (UserDataAtom *)((long)atom + EndianS32_BtoN(atom->size));
- } while (moreAtoms);
- }
- // turn on the sound clock for two channels
- cmd.cmd = clockComponentCmd;
- cmd.param1 = true;
- err = SndDoImmediate(gChan1, &cmd);
- if (err != noErr) return (err);
- err = SndDoImmediate(gChan2, &cmd);
- if (err != noErr) return (err);
- // get the sound clock component from one channel
- cmd.cmd = getClockComponentCmd;
- cmd.param2 = (long)&clock;
- err = SndDoImmediate(gChan1, &cmd);
- if (err != noErr) return (err);
- // get the current time from one channel
- scheduledSound1.startTime.base = nil;
- err = ClockGetTime(clock, &scheduledSound1.startTime);
- if (err != noErr) return (err);
- // add 1/2 second to the current time
- deltaTime.value.hi = 0;
- deltaTime.value.lo = 1;
- deltaTime.scale = 2;
- AddTime(&scheduledSound1.startTime, &deltaTime);
- // schedule two sounds to play at the same starting time
- scheduledSound1.flags = kScheduledSoundDoScheduled;
- scheduledSound2.flags = kScheduledSoundDoScheduled;
- scheduledSound2.startTime = scheduledSound1.startTime;
- cmd.cmd = scheduledSoundCmd;
- cmd.param2 = (long)&scheduledSound1;
- err = SndDoImmediate(gChan1, &cmd);
- if (err != noErr) return (err);
- cmd.param2 = (long)&scheduledSound2;
- err = SndDoImmediate(gChan2, &cmd);
- if (err != noErr) return (err);